diff options
| author | joonhoekim <26rote@gmail.com> | 2025-07-23 06:06:27 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-07-23 06:06:27 +0000 |
| commit | f9bfc82880212e1a13f6bbb28ecfc87b89346f26 (patch) | |
| tree | ee792f340ebfa7eaf30d2e79f99f41213e5c5cf3 /app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx | |
| parent | edc0eabc8f5fc44408c28023ca155bd73ddf8183 (diff) | |
(김준회) 메뉴접근제어(부서별) 메뉴 구현
Diffstat (limited to 'app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx new file mode 100644 index 00000000..277511cb --- /dev/null +++ b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx @@ -0,0 +1,384 @@ +"use client"; + +import * as React from "react"; +import { Loader2, Users, Building2, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + DepartmentNode +} from "@/lib/users/knox-service"; +import { + getDepartmentDomainAssignmentsByDepartments, + type UserDomain +} from "@/lib/users/department-domain/service"; +import { DOMAIN_OPTIONS, getDomainLabel } from "./domain-constants"; + +interface ExistingAssignment { + id: number; + companyCode: string; + departmentCode: string; + departmentName: string; + assignedDomain: string; + description?: string | null; + createdAt: Date; + updatedAt: Date; +} + +interface DepartmentDomainAssignmentDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedDepartments: string[]; + departments: DepartmentNode[]; + companyInfo: { code: string; name: string }; + onAssign: (assignments: { + departmentCodes: string[]; + domain: string; + description?: string; + }) => Promise<void>; + isLoading?: boolean; +} + +export function DepartmentDomainAssignmentDialog({ + open, + onOpenChange, + selectedDepartments, + departments, + companyInfo, + onAssign, + isLoading = false, +}: DepartmentDomainAssignmentDialogProps) { + const [selectedDomain, setSelectedDomain] = React.useState<string>(""); + const [description, setDescription] = React.useState<string>(""); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [existingAssignments, setExistingAssignments] = React.useState<ExistingAssignment[]>([]); + const [isLoadingAssignments, setIsLoadingAssignments] = React.useState(false); + + // 선택된 부서들의 정보 가져오기 + const getSelectedDepartmentInfo = React.useCallback(() => { + const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => { + for (const node of nodes) { + if (node.departmentCode === code) { + return node; + } + const found = findDepartment(node.children, code); + if (found) return found; + } + return null; + }; + + return selectedDepartments + .map(code => findDepartment(departments, code)) + .filter(Boolean) as DepartmentNode[]; + }, [departments, selectedDepartments]); + + // 회사별로 그룹화 + const selectedDepartmentsByCompany = React.useMemo(() => { + const deptInfo = getSelectedDepartmentInfo(); + const grouped = new Map<string, DepartmentNode[]>(); + + deptInfo.forEach(dept => { + if (!grouped.has(dept.companyCode)) { + grouped.set(dept.companyCode, []); + } + grouped.get(dept.companyCode)!.push(dept); + }); + + return grouped; + }, [getSelectedDepartmentInfo]); + + // 기존 할당 정보 조회 + React.useEffect(() => { + if (open && selectedDepartments.length > 0) { + const loadExistingAssignments = async () => { + setIsLoadingAssignments(true); + try { + const assignments = await getDepartmentDomainAssignmentsByDepartments(selectedDepartments); + setExistingAssignments(assignments as ExistingAssignment[]); + } catch (error) { + console.error("기존 할당 정보 조회 실패:", error); + setExistingAssignments([]); + } finally { + setIsLoadingAssignments(false); + } + }; + + loadExistingAssignments(); + } else { + setExistingAssignments([]); + } + }, [open, selectedDepartments]); + + // 폼 초기화 + React.useEffect(() => { + if (open) { + setSelectedDomain(""); + setDescription(""); + setIsSubmitting(false); + } + }, [open]); + + // 할당 처리 + const handleAssign = async () => { + if (!selectedDomain || selectedDepartments.length === 0) { + return; + } + + setIsSubmitting(true); + + try { + await onAssign({ + departmentCodes: selectedDepartments, + domain: selectedDomain, + description: description.trim() || undefined, + }); + + // 성공 시 다이얼로그 닫기 + onOpenChange(false); + } catch (error) { + console.error("도메인 할당 실패:", error); + } finally { + setIsSubmitting(false); + } + }; + + const canSubmit = selectedDomain && selectedDepartments.length > 0 && !isSubmitting && !isLoading; + const selectedDomainInfo = DOMAIN_OPTIONS.find(opt => opt.value === selectedDomain); + const hasConflicts = existingAssignments.some(a => a.assignedDomain !== selectedDomain && selectedDomain); + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[90vh] overflow-hidden flex flex-col"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Building2 className="h-5 w-5" /> + 부서별 도메인 할당 + </DialogTitle> + <DialogDescription> + 선택된 {selectedDepartments.length}개 부서에 도메인을 할당합니다. + 상위 부서를 선택한 경우 하위 부서들도 자동으로 포함됩니다. + </DialogDescription> + </DialogHeader> + + <div className="flex-1 overflow-hidden"> + <ScrollArea className="h-full pr-4"> + <div className="space-y-6"> + {/* 선택된 부서들 표시 */} + <div className="space-y-3"> + <Label className="text-sm font-medium flex items-center gap-2"> + <Users className="h-4 w-4" /> + 선택된 부서 ({selectedDepartments.length}개) + </Label> + + <div className="border rounded-md p-3 max-h-32 overflow-y-auto"> + {Array.from(selectedDepartmentsByCompany.entries()).map(([companyCode, depts]) => ( + <div key={companyCode} className="mb-3 last:mb-0"> + <div className="text-sm font-medium text-muted-foreground mb-2"> + {companyCode} - {companyInfo.name} + </div> + <div className="flex flex-wrap gap-2"> + {depts.map((dept) => ( + <Badge + key={dept.departmentCode} + variant="outline" + className="text-xs" + > + {dept.departmentName || dept.departmentCode} + </Badge> + ))} + </div> + </div> + ))} + </div> + </div> + + {/* 기존 할당 현황 */} + {(existingAssignments.length > 0 || isLoadingAssignments) && ( + <> + <Separator /> + <div className="space-y-3"> + <Label className="text-sm font-medium flex items-center gap-2"> + <AlertCircle className="h-4 w-4" /> + 현재 할당 현황 + </Label> + + {isLoadingAssignments ? ( + <div className="flex items-center justify-center py-4"> + <Loader2 className="h-4 w-4 animate-spin mr-2" /> + 기존 할당 정보를 조회하는 중... + </div> + ) : ( + <div className="border rounded-md"> + <Table> + <TableHeader> + <TableRow> + <TableHead>부서</TableHead> + <TableHead>현재 도메인</TableHead> + <TableHead>할당일</TableHead> + <TableHead>설명</TableHead> + </TableRow> + </TableHeader> + <TableBody> + {existingAssignments.map((assignment) => ( + <TableRow key={assignment.id}> + <TableCell className="font-medium"> + {assignment.departmentName} + </TableCell> + <TableCell> + <Badge + variant={assignment.assignedDomain === 'evcp' ? 'default' : 'secondary'} + > + {getDomainLabel(assignment.assignedDomain)} + </Badge> + </TableCell> + <TableCell className="text-sm text-muted-foreground"> + {new Date(assignment.createdAt).toLocaleDateString('ko-KR')} + </TableCell> + <TableCell className="max-w-xs truncate text-sm"> + {assignment.description || '-'} + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + )} + + {hasConflicts && ( + <div className="bg-yellow-50 border-yellow-200 border rounded-md p-3"> + <div className="flex items-start gap-2"> + <AlertCircle className="h-4 w-4 text-yellow-600 mt-0.5" /> + <div className="text-sm"> + <div className="font-medium text-yellow-800">도메인 변경 주의</div> + <div className="text-yellow-700"> + 일부 부서의 기존 도메인과 다른 도메인을 할당하려고 합니다. + 기존 할당은 자동으로 비활성화됩니다. + </div> + </div> + </div> + </div> + )} + </div> + </> + )} + + <Separator /> + + {/* 도메인 선택 */} + <div className="space-y-2"> + <Label htmlFor="domain-select" className="text-sm font-medium"> + 할당할 도메인 * + </Label> + <Select value={selectedDomain} onValueChange={setSelectedDomain}> + <SelectTrigger id="domain-select"> + <SelectValue placeholder="도메인을 선택하세요" /> + </SelectTrigger> + <SelectContent> + {DOMAIN_OPTIONS.map((option) => ( + <SelectItem key={option.value} value={option.value}> + <div className="flex flex-col"> + <span className="font-medium">{option.label}</span> + <span className="text-xs text-muted-foreground"> + {option.description} + </span> + </div> + </SelectItem> + ))} + </SelectContent> + </Select> + + {selectedDomainInfo && ( + <div className="text-sm text-muted-foreground"> + <Badge variant="secondary" className="mr-2"> + {selectedDomainInfo.label} + </Badge> + {selectedDomainInfo.description} + </div> + )} + </div> + + {/* 할당 사유/설명 */} + <div className="space-y-2"> + <Label htmlFor="description" className="text-sm font-medium"> + 할당 사유 또는 설명 (선택사항) + </Label> + <Textarea + id="description" + placeholder="예: 구매 업무 담당자들에게 procurement 도메인 할당" + value={description} + onChange={(e) => setDescription(e.target.value)} + rows={3} + maxLength={500} + /> + <div className="text-xs text-muted-foreground text-right"> + {description.length}/500 + </div> + </div> + + {/* 주의사항 */} + <div className="bg-muted/50 p-3 rounded-md"> + <div className="text-sm text-muted-foreground"> + <div className="font-medium mb-1">⚠️ 주의사항</div> + <ul className="list-disc list-inside space-y-1 text-xs"> + <li>도메인 할당은 해당 부서 소속 사용자들의 메뉴 접근 권한에 영향을 줍니다.</li> + <li>기존에 다른 도메인이 할당된 부서는 새로운 도메인으로 덮어씌워집니다.</li> + <li>Knox 조직도 변경으로 인해 부서가 삭제된 경우, 해당 할당은 고립된 레코드가 됩니다.</li> + </ul> + </div> + </div> + </div> + </ScrollArea> + </div> + + <DialogFooter className="border-t pt-4"> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isSubmitting || isLoading} + > + 취소 + </Button> + <Button + onClick={handleAssign} + disabled={!canSubmit} + > + {isSubmitting || isLoading ? ( + <> + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + 할당 중... + </> + ) : ( + `도메인 할당 (${selectedDepartments.length}개 부서)` + )} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + ); +}
\ No newline at end of file |
